package com.gitee.l0km.xthrift.thrift.codec;

import static java.lang.String.format;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolException;

import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.util.TypeUtils;
import com.facebook.swift.codec.ThriftCodec;
import com.facebook.swift.codec.ThriftCodecManager;
import com.facebook.swift.codec.ThriftField.Requiredness;
import com.facebook.swift.codec.internal.TProtocolReader;
import com.facebook.swift.codec.internal.TProtocolWriter;
import com.facebook.swift.codec.internal.reflection.ReflectionThriftStructCodec;
import static com.facebook.swift.codec.metadata.FieldKind.THRIFT_FIELD;
import com.facebook.swift.codec.metadata.ThriftConstructorInjection;
import com.facebook.swift.codec.metadata.ThriftFieldInjection;
import com.facebook.swift.codec.metadata.ThriftFieldMetadata;
import com.facebook.swift.codec.metadata.ThriftInjection;
import com.facebook.swift.codec.metadata.ThriftMethodInjection;
import com.facebook.swift.codec.metadata.ThriftParameterInjection;
import com.facebook.swift.codec.metadata.ThriftStructMetadata;
import com.gitee.l0km.beanfilter.core.CodecPhase;
import com.gitee.l0km.beanfilter.core.CodecScene;
import com.gitee.l0km.beanfilter.core.Filterable;
import com.gitee.l0km.beanfilter.core.IFilter;
import com.gitee.l0km.beanfilter.core.LocalSupplyFilter;
import com.gitee.l0km.beanfilter.core.RequestPhase;
import com.gitee.l0km.beanfilter.core.SupplyFilter;
import com.gitee.l0km.beanfilter.core.context.LocalContext;
import com.gitee.l0km.xthrift.thrift.protocol.TProtocolReaderXthrift;
import static com.gitee.l0km.xthrift.thrift.protocol.TProtocolSupport.getFieldName;
import com.gitee.l0km.xthrift.thrift.protocol.TProtocolWriterXthrift;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.reflect.TypeToken;

/**
 * 支持字段过滤的 Java Bean 类型编解码器实现
 * @author guyadong
 * @since 2.11.0
 *
 * @param <T>
 */
public class FilterableThriftStructCodec<T> extends ReflectionThriftStructCodec<T> implements Filterable{
	/** 编码过滤器 */
	private final LocalSupplyFilter  serliaizationLocalSupplyFilter;
	/** 解码过滤器 */
	private final LocalSupplyFilter  deserliaizationLocalSupplyFilter;
	/** 字段名与字段ID的映射 */
	private final ImmutableBiMap<String, Short> fieldIds;
	/** 字段的 thrift ID与字段名对照表 */
	private final ImmutableSortedMap<Short, String> fieldNames;
	protected final Class<?> structClass;
	/**
	 * 构造方法
	 * @param manager
	 * @param metadata
	 */
	public FilterableThriftStructCodec(ThriftCodecManager manager, ThriftStructMetadata metadata) {
		super(manager, metadata);
		this.structClass = metadata.getStructClass();
		serliaizationLocalSupplyFilter = new LocalSupplyFilter(structClass,RequestPhase.SERIALIZING,CodecScene.THRIFT);
		deserliaizationLocalSupplyFilter = new LocalSupplyFilter(structClass,RequestPhase.DESERIALIZING,CodecScene.THRIFT);
        ImmutableBiMap.Builder<String, Short> builder =  ImmutableBiMap.builder();
        for (ThriftFieldMetadata fieldMetadata : metadata.getFields(THRIFT_FIELD)) {
            builder.put(fieldMetadata.getName(), fieldMetadata.getId());
        }
        this.fieldIds = builder.build();
        this.fieldNames = ImmutableSortedMap.copyOf(fieldIds.inverse());
	}

	protected SupplyFilter getSerliaizationSupplyFilter() {
		return serliaizationLocalSupplyFilter.getSupplyFilter();
	}

	protected SupplyFilter getDeserliaizationSupplyFilter() {
		return deserliaizationLocalSupplyFilter.getSupplyFilter();
	}
	/**
	 * 返回字段名对应的字段ID,没找到返回{@code null}
	 * @param fieldName
	 */
	protected Short fieldIdOrnullOf(String fieldName) {
		if(null != fieldName) {
			for(Entry<Short, String> entry : fieldNames.entrySet()) {
				if(entry.getValue().equals(fieldName)) {
					return entry.getKey();
				}
			}
		}
		return null;
	}
	/**
	 * 返回字段名对应的字段ID,没找到抛出异常
	 * @param fieldName
	 */
	protected short fieldIdOf(String fieldName) {
		return checkNotNull(fieldIdOrnullOf(fieldName),"INVALID field name %s",fieldName);
	}
	@Override
	public void injectFilter(Supplier<? extends IFilter> fieldFilterSupplier,
			Supplier<? extends IFilter> valueFilterSupplier, CodecPhase codecPhase) {
		checkNotNull(codecPhase,"activePhase is null");
		if(null != fieldFilterSupplier || null !=valueFilterSupplier) {
			SupplyFilter supplyFilter= SupplyFilter.combine2(fieldFilterSupplier,valueFilterSupplier);
			if(codecPhase.serialization) {
				serliaizationLocalSupplyFilter.setSupplyFilter(supplyFilter);;
			}
			if(codecPhase.deserialization) {
				deserliaizationLocalSupplyFilter.setSupplyFilter(supplyFilter);;
			}
		}
	}

	/**
	 * 值类似转换<br>
	 * 如果输入值与字段类型不匹配则调用
	 * @param field
	 * @param value
	 */
	protected static Object cast(ThriftFieldMetadata field,Object value) {
		if(null != value) {
			Class<?> fieldType = TypeToken.of(field.getThriftType().getJavaType()).getRawType();
			if(!fieldType.isAssignableFrom(value.getClass())){
				/** 确保 IValueFilter 返回类型与字段类型匹配 */
				value = TypeUtils.cast(value, fieldType, ParserConfig.getGlobalInstance());
			}
		}
		return value;
	}
	@Override
    public T read(TProtocol protocol) throws Exception {
		try {
			LocalContext.enteringEnv(null, RequestPhase.DESERIALIZING, CodecScene.THRIFT);
			return doRead(protocol);
		} finally {
			LocalContext.exitingEnv();
		}
    }
	@Override
	public void write(T instance, TProtocol protocol) throws Exception {
		try {
			LocalContext.enteringEnv(instance, RequestPhase.SERIALIZING, CodecScene.THRIFT);
			doWrite(instance,protocol);
		} finally {
			LocalContext.exitingEnv();
		}
	}
	/**
	 * 解析字段,子类可以重写此方法实现自定义逻辑
	 * @since 1.2.0
	 */
	protected Object readField(TProtocolReader reader,ThriftCodec<?> codec ) throws Exception {
		return reader.readField(codec);
	}
	protected T doRead(TProtocol protocol)
			throws Exception
	{
        TProtocolReader reader = TProtocolReaderXthrift.of(protocol);
        reader.readStructBegin();
		/** 输入中存在的字段 */
		LinkedHashSet<String> deserialized = new LinkedHashSet<>();
		/** 被过滤的字段 */
		LinkedHashSet<String> modifiedReset = new LinkedHashSet<>();
		/** 值过滤时被额外添加非null值的字段 */
		LinkedHashSet<String> modifiedSet = new LinkedHashSet<>();
        Map<Short, Object> data = new HashMap<>(metadata.getFields().size());
        while (reader.nextField()) {
            short fieldId = reader.getFieldId();
            if(fieldId == 0) {
            	/** 如果输入中没有指定字段ID则尝试从字段名获取字段ID */
            	String fieldName = getFieldName(reader);
            	checkArgument(!isNullOrEmpty(fieldName),"field name is empty or null");
            	Short _fieldId = fieldIds.get(fieldName);
				if (null != _fieldId) {
					fieldId = _fieldId;
				}
            }
            // do we have a codec for this field
            ThriftCodec<?> codec = fields.get(fieldId);
            if (codec == null) {
                reader.skipFieldData();
                continue;
            }

            // is this field readable
            ThriftFieldMetadata field = metadata.getField(fieldId);
            if (field.isReadOnly() || field.getType() != THRIFT_FIELD) {
                reader.skipFieldData();
                continue;
            }
            String fieldName = fieldNames.get(fieldId);
            deserialized.add(fieldName);
            /** 字段过滤 */
            if(!getDeserliaizationSupplyFilter().permit(structClass, fieldName)) {
            	modifiedReset.add(fieldName);
            	reader.skipFieldData();
            	continue;
            }
            // read the value
            Object value = readField(reader,codec);
            /** 值过滤 */
            value = process(field, fieldName, value, getDeserliaizationSupplyFilter());
            if (value == null) {
              if (field.getRequiredness() == Requiredness.REQUIRED) {
                throw new TProtocolException("required field was not set");
              } else {
                continue;
              }
            }
            data.put(fieldId, value);
        }
        processUndeserialized(data,deserialized, modifiedSet);
        reader.readStructEnd();

        // build the struct
        return afterConstruct(constructStruct(data),modifiedReset, modifiedSet);
    }
    protected void doWrite(T instance, TProtocol protocol) throws Exception {
	    TProtocolWriter writer = TProtocolWriterXthrift.of(protocol);
	    writer.writeStructBegin(metadata.getStructName());
	
	    for (ThriftFieldMetadata fieldMetadata : metadata.getFields(THRIFT_FIELD)) {
	        // is the field readable?
	        if (fieldMetadata.isWriteOnly()) {
	            continue;
	        }
	        /** 字段过滤 */
            if(getSerliaizationSupplyFilter().permit(structClass, fieldMetadata.getName())) {
            	// get the field value
            	Object fieldValue = getFieldValue(instance, fieldMetadata);
            	fieldValue = process(fieldMetadata, fieldMetadata.getName(), fieldValue, getSerliaizationSupplyFilter());
    			// write the field
    			if (fieldValue != null) {
    				@SuppressWarnings("unchecked")
    				ThriftCodec<Object> codec = (ThriftCodec<Object>) fields.get(fieldMetadata.getId());
    				writer.writeField(fieldMetadata.getName(), fieldMetadata.getId(), codec, fieldValue);
    			}
            }
	    }
	    writer.writeStructEnd();
	}
	/**
	 * 值过滤
	 * @param field
	 * @param fieldName
	 * @param value
	 * @param supplyFilter
	 */
	protected Object process(ThriftFieldMetadata field,String fieldName, Object value, SupplyFilter supplyFilter) {
		Object processed = supplyFilter.process(structClass, fieldName, value);
		return cast(field,processed);
	}

	/**
	 * 对反序列化时不存在的参数字段进行值过滤
	 * @param data
	 * @param deserialized 输入中存在的字段
	 * @param additionalSet [out]保存不存在的参数字段进行值过滤返回为非{@code null}的值的字段
	 * @return always data
	 */
	protected Map<Short, Object> processUndeserialized(Map<Short, Object> data,Set<String> deserialized, Set<String> additionalSet) {
		for (ThriftFieldMetadata fieldMetadata : metadata.getFields(THRIFT_FIELD)) {
			if(!deserialized.contains(fieldMetadata.getName())) {
				/** 字段过滤 */
	            if(getDeserliaizationSupplyFilter().permit(structClass, fieldMetadata.getName())) {
	            	Object value = process(fieldMetadata, fieldMetadata.getName(), null, getDeserliaizationSupplyFilter());
	            	if(null != value) {
	            		data.put(fieldMetadata.getId(), value);
	            		additionalSet.add(fieldMetadata.getName());
	            	}
	            }
			}
		}
		return data;
	}
	protected T afterConstruct(T instance, Set<String> modifiedReset, Set<String> modifiedSet) {
		return instance;
	}
	/**
     * copy from {@link com.gitee.l0km.xthrift.thrift.codec.FilterableThriftStructCodec#constructStruct(Map)}
     */
    @SuppressWarnings("unchecked")
    protected T constructStruct(Map<Short, Object> data)
            throws Exception
    {
        // construct instance
        Object instance;
        {
            ThriftConstructorInjection constructor = metadata.getConstructorInjection().get();
            Object[] parametersValues = new Object[constructor.getParameters().size()];
            for (ThriftParameterInjection parameter : constructor.getParameters()) {
                Object value = data.get(parameter.getId());
                parametersValues[parameter.getParameterIndex()] = value;
            }

            try {
                instance = constructor.getConstructor().newInstance(parametersValues);
            }
            catch (InvocationTargetException e) {
                if (e.getTargetException() != null) {
                    Throwables.throwIfInstanceOf(e.getTargetException(), Exception.class);
                }
                throw e;
            }
        }

        // inject fields
        for (ThriftFieldMetadata fieldMetadata : metadata.getFields(THRIFT_FIELD)) {
            for (ThriftInjection injection : fieldMetadata.getInjections()) {
                if (injection instanceof ThriftFieldInjection) {
                    ThriftFieldInjection fieldInjection = (ThriftFieldInjection) injection;
                    Object value = data.get(fieldInjection.getId());
                    if (value != null) {
                        fieldInjection.getField().set(instance, value);
                    }
                }
            }
        }

        // inject methods
        for (ThriftMethodInjection methodInjection : metadata.getMethodInjections()) {
            boolean shouldInvoke = false;
            Object[] parametersValues = new Object[methodInjection.getParameters().size()];
            for (ThriftParameterInjection parameter : methodInjection.getParameters()) {
                Object value = data.get(parameter.getId());
                if (value != null) {
                    parametersValues[parameter.getParameterIndex()] = value;
                    shouldInvoke = true;
                }
            }

            if (shouldInvoke) {
                try {
                    methodInjection.getMethod().invoke(instance, parametersValues);
                }
                catch (InvocationTargetException e) {
                    if (e.getTargetException() != null) {
                        Throwables.throwIfInstanceOf(e.getTargetException(), Exception.class);
                    }
                    throw e;
                }
            }
        }

        // builder method
        if (metadata.getBuilderMethod().isPresent()) {
            ThriftMethodInjection builderMethod = metadata.getBuilderMethod().get();
            Object[] parametersValues = new Object[builderMethod.getParameters().size()];
            for (ThriftParameterInjection parameter : builderMethod.getParameters()) {
                Object value = data.get(parameter.getId());
                parametersValues[parameter.getParameterIndex()] = value;
            }

            try {
                instance = builderMethod.getMethod().invoke(instance, parametersValues);
                if (instance == null) {
                    throw new IllegalArgumentException("Builder method returned a null instance");

                }
                if (!metadata.getStructClass().isInstance(instance)) {
                    throw new IllegalArgumentException(format("Builder method returned instance of type %s, but an instance of %s is required",
                            instance.getClass().getName(),
                            metadata.getStructClass().getName()));
                }
            }
            catch (InvocationTargetException e) {
                if (e.getTargetException() != null) {
                    Throwables.throwIfInstanceOf(e.getTargetException(), Exception.class);
                }
                throw e;
            }
        }

        return (T) instance;
    }

}
